/*
 * (c) Copyright 2020 CORSIKA Project, corsika-project@lists.kit.edu
 *
 * This software is distributed under the terms of the 3-clause BSD license.
 * See file LICENSE for a full version of the license.
 */

#include <catch2/catch_all.hpp>

#include <cmath>
#include <corsika/framework/core/PhysicalUnits.hpp>
#include <corsika/framework/geometry/Box.hpp>
#include <corsika/framework/geometry/SeparationPlane.hpp>
#include <corsika/framework/geometry/CoordinateSystem.hpp>
#include <corsika/framework/geometry/Helix.hpp>
#include <corsika/framework/geometry/LeapFrogTrajectory.hpp>
#include <corsika/framework/geometry/Line.hpp>
#include <corsika/framework/geometry/Path.hpp>
#include <corsika/framework/geometry/Point.hpp>
#include <corsika/framework/geometry/RootCoordinateSystem.hpp>
#include <corsika/framework/geometry/Sphere.hpp>
#include <corsika/framework/geometry/StraightTrajectory.hpp>

#include <tests/common/SetupStack.hpp>

#include <PhysicalUnitsCatch2.hpp> // namespace corsike::testing

using namespace corsika;
using namespace corsika::testing;
using Catch::Approx;

double constexpr absMargin = 1.0e-8;

TEST_CASE("Geometry CoordinateSystems") {

  logging::set_level(logging::level::info);

  CoordinateSystemPtr rootCS = get_root_CoordinateSystem();

  QuantityVector<length_d> const coordinates{0_m, 0_m, 0_m};
  Point p1(rootCS, coordinates);
  CORSIKA_LOG_INFO("Point p1={}", p1);

  QuantityVector<magnetic_flux_density_d> components{1. * tesla, 0. * tesla, 0. * tesla};
  Vector<magnetic_flux_density_d> v1(rootCS, components);
  CORSIKA_LOG_INFO("Vector<magnetic_flux_density_d> v1={}", v1);

  CHECK((p1.getCoordinates() - coordinates).getNorm().magnitude() ==
        Approx(0).margin(absMargin));
  CHECK((p1.getCoordinates(rootCS) - coordinates).getNorm().magnitude() ==
        Approx(0).margin(absMargin));

  SECTION("basic operations") {
    auto testV0 = v1 * 6;
    CHECK(testV0.getNorm() / tesla == Approx(6));
    auto testV1 = 6 * v1;
    CHECK(testV1.getNorm() / tesla == Approx(6));
    auto testV2 = 6_m * v1;
    CHECK(testV2.getNorm() / (tesla * meter) == Approx(6));
    auto testV3 = v1 * 6_m;
    CHECK(testV3.getNorm() / (tesla * meter) == Approx(6));
  }

  SECTION("point") {
    Point const p00 = Point(rootCS, {0_m, 0_m, 0_m});
    Point const p01 = Point(rootCS, {3_m, 3_m, 3_m});
    Point const p02 = Point(rootCS, {1_m, -5_m, 6_m});

    LengthVector const d01 = p00 - p01;
    LengthVector const d12 = p01 - p02;
    LengthVector const d20 = p02 - p00;

    CHECK(d12.getNorm() / (p01 - p02).getNorm() == Approx(1));
    CHECK(d12.getNorm() / distance(p01, p02) == Approx(1));
    CHECK(d12.getNorm() / distance(p02, p01) == Approx(1));
  }

  SECTION("translations") {
    QuantityVector<length_d> const translationVector{0_m, 4_m, 0_m};
    CORSIKA_LOG_INFO("QuantityVector<length_d> translationVector={}", translationVector);

    CoordinateSystemPtr translatedCS = make_translation(rootCS, translationVector);

    CHECK(translatedCS->getReferenceCS() == rootCS);

    CHECK((p1.getCoordinates(translatedCS) + translationVector).getNorm().magnitude() ==
          Approx(0).margin(absMargin));

    // Vectors are not subject to translations
    CHECK((v1.getComponents(rootCS) - v1.getComponents(translatedCS))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));

    Point p2(translatedCS, {0_m, 0_m, 0_m});
    CHECK(((p2 - p1).getComponents() - translationVector).getNorm().magnitude() ==
          Approx(0).margin(absMargin));
    CHECK(p2.getX(rootCS) == 0_m);
    CHECK(p2.getY(rootCS) == 4_m);
    CHECK(p2.getZ(rootCS) == 0_m);
    CHECK(p2.getX(translatedCS) == 0_m);
    CHECK(p2.getY(translatedCS) == 0_m);
    CHECK(p2.getZ(translatedCS) == 0_m);

    Vector<magnetic_flux_density_d> v2(translatedCS, components);
    CHECK((v2 - v1).getNorm() / 1_T == Approx(0));
  }

  SECTION("multiple translations") {
    QuantityVector<length_d> const tv1{0_m, 5_m, 0_m};
    CoordinateSystemPtr cs2 = make_translation(rootCS, tv1);

    QuantityVector<length_d> const tv2{3_m, 0_m, 0_m};
    CoordinateSystemPtr cs3 = make_translation(rootCS, tv2);

    QuantityVector<length_d> const tv3{0_m, 0_m, 2_m};
    CoordinateSystemPtr cs4 = make_translation(cs3, tv3);

    CHECK(cs4->getReferenceCS()->getReferenceCS() == rootCS);

    CHECK(get_transformation(*cs3.get(), *cs2.get())
              .isApprox(make_translation(rootCS, {3_m, -5_m, 0_m})->getTransform()));
    CHECK(get_transformation(*cs2.get(), *cs3.get())
              .isApprox(make_translation(rootCS, {-3_m, +5_m, 0_m})->getTransform()));
  }

  SECTION("rotations") {
    QuantityVector<length_d> const axis{0_m, 0_m, 1_km};
    double const angle = 90. / 180. * M_PI;

    CoordinateSystemPtr rotatedCS = make_rotation(rootCS, axis, angle);
    CHECK(rotatedCS->getReferenceCS() == rootCS);

    CHECK(v1.getComponents(rotatedCS)[1].magnitude() ==
          Approx((-1. * tesla).magnitude()));

    // vector norm invariant under rotation
    CHECK(v1.getComponents(rotatedCS).getNorm().magnitude() ==
          Approx(v1.getComponents(rootCS).getNorm().magnitude()));

    // this is not possible
    QuantityVector<length_d> const axis_invalid{0_m, 0_m, 0_km};
    CHECK_THROWS(make_rotation(rootCS, axis_invalid, angle));
  }

  SECTION("multiple rotations") {
    QuantityVector<length_d> const zAxis{0_m, 0_m, 1_km};
    QuantityVector<length_d> const yAxis{0_m, 7_nm, 0_m};
    QuantityVector<length_d> const xAxis{2_m, 0_nm, 0_m};

    QuantityVector<magnetic_flux_density_d> components{1. * tesla, 2. * tesla,
                                                       3. * tesla};
    Vector<magnetic_flux_density_d> v1(rootCS, components);

    double const angle = 90. / 180. * M_PI;

    CoordinateSystemPtr rotated1 = make_rotation(rootCS, zAxis, angle);
    CoordinateSystemPtr rotated2 = make_rotation(rotated1, yAxis, angle);
    CoordinateSystemPtr rotated3 = make_rotation(rotated2, zAxis, -angle);

    CoordinateSystemPtr combined = make_rotation(rootCS, xAxis, -angle);

    auto comp1 = v1.getComponents(rotated3);
    auto comp3 = v1.getComponents(combined);
    CHECK((comp1 - comp3).getNorm().magnitude() == Approx(0).margin(absMargin));
  }

  SECTION("RotateToZ positive") {
    Vector const v{rootCS, 0_m, 1_m, 1_m};
    auto const csPrime = make_rotationToZ(rootCS, v);
    Vector const zPrime{csPrime, 0_m, 0_m, 5_m};
    Vector const xPrime{csPrime, 5_m, 0_m, 0_m};
    Vector const yPrime{csPrime, 0_m, 5_m, 0_m};

    CHECK(xPrime.dot(v).magnitude() == Approx(0).margin(absMargin));
    CHECK(yPrime.dot(v).magnitude() == Approx(0).margin(absMargin));
    CHECK((zPrime.dot(v) / 1_m).magnitude() == Approx(5 * sqrt(2)));

    CHECK(zPrime.getComponents(rootCS)[1].magnitude() ==
          Approx(zPrime.getComponents(rootCS)[2].magnitude()));
    CHECK(zPrime.getComponents(rootCS)[0].magnitude() == Approx(0));

    CHECK(xPrime.getComponents(rootCS).getEigenVector().dot(
              yPrime.getComponents(rootCS).getEigenVector()) == Approx(0));
    CHECK(zPrime.getComponents(rootCS).getEigenVector().dot(
              xPrime.getComponents(rootCS).getEigenVector()) == Approx(0));
    CHECK(yPrime.getComponents(rootCS).getEigenVector().dot(
              zPrime.getComponents(rootCS).getEigenVector()) == Approx(0));

    CHECK(yPrime.getComponents(rootCS).getEigenVector().dot(
              yPrime.getComponents(rootCS).getEigenVector()) ==
          Approx((5_m * 5_m).magnitude()));
    CHECK(xPrime.getComponents(rootCS).getEigenVector().dot(
              xPrime.getComponents(rootCS).getEigenVector()) ==
          Approx((5_m * 5_m).magnitude()));
    CHECK(zPrime.getComponents(rootCS).getEigenVector().dot(
              zPrime.getComponents(rootCS).getEigenVector()) ==
          Approx((5_m * 5_m).magnitude()));
  }

  SECTION("RotateToZ negative") {
    Vector const v{rootCS, 0_m, 0_m, -1_m};
    auto const csPrime = make_rotationToZ(rootCS, v);
    Vector const zPrime{csPrime, 0_m, 0_m, 5_m};
    Vector const xPrime{csPrime, 5_m, 0_m, 0_m};
    Vector const yPrime{csPrime, 0_m, 5_m, 0_m};

    CHECK(zPrime.dot(v).magnitude() > 0);
    CHECK(xPrime.getComponents(rootCS).getEigenVector().dot(
              v.getComponents().getEigenVector()) == Approx(0));
    CHECK(yPrime.getComponents(rootCS).getEigenVector().dot(
              v.getComponents().getEigenVector()) == Approx(0));

    CHECK(xPrime.getComponents(rootCS).getEigenVector().dot(
              yPrime.getComponents(rootCS).getEigenVector()) == Approx(0));
    CHECK(zPrime.getComponents(rootCS).getEigenVector().dot(
              xPrime.getComponents(rootCS).getEigenVector()) == Approx(0));
    CHECK(yPrime.getComponents(rootCS).getEigenVector().dot(
              zPrime.getComponents(rootCS).getEigenVector()) == Approx(0));

    CHECK(yPrime.getComponents(rootCS).getEigenVector().dot(
              yPrime.getComponents(rootCS).getEigenVector()) ==
          Approx((5_m * 5_m).magnitude()));
    CHECK(xPrime.getComponents(rootCS).getEigenVector().dot(
              xPrime.getComponents(rootCS).getEigenVector()) ==
          Approx((5_m * 5_m).magnitude()));
    CHECK(zPrime.getComponents(rootCS).getEigenVector().dot(
              zPrime.getComponents(rootCS).getEigenVector()) ==
          Approx((5_m * 5_m).magnitude()));
  }
}

TEST_CASE("Geometry CoordinateSystem-hirarchy") {

  CoordinateSystemPtr rootCS = get_root_CoordinateSystem();

  CHECK(get_transformation(*rootCS.get(), *rootCS.get())
            .isApprox(EigenTransform::Identity()));

  // define the root coordinate system
  CoordinateSystemPtr root = get_root_CoordinateSystem();
  Point const p1(root, {0_m, 0_m, 0_m}); // the origin of the root CS

  CHECK(p1.getX(root) == 0_m);
  CHECK(p1.getY(root) == 0_m);
  CHECK(p1.getZ(root) == 0_m);

  // root -> cs2
  CoordinateSystemPtr cs2 = make_translation(root, {0_m, 0_m, 1_m});
  Point const p2(cs2, {0_m, 0_m, -1_m});

  // root -> cs2 -> cs3
  CoordinateSystemPtr cs3 = make_translation(cs2, {0_m, 0_m, -1_m});
  Point const p3(cs3, {0_m, 0_m, 0_m});

  // root -> cs2 -> cs4
  CoordinateSystemPtr cs4 = make_translation(cs2, {0_m, 0_m, -1_m});
  Point const p4(cs4, {0_m, 0_m, 0_m});

  // root -> cs2 -> cs4 -> cs5
  CoordinateSystemPtr cs5 =
      make_rotation(cs4, QuantityVector<length_d>{1_m, 0_m, 0_m}, 90 * degree_angle);
  Point const p5(cs5, {0_m, 0_m, 0_m});

  // root -> cs6
  CoordinateSystemPtr cs6 =
      make_rotation(root, QuantityVector<length_d>{1_m, 0_m, 0_m}, 90 * degree_angle);
  Point const p6(cs6, {0_m, 0_m, 0_m}); // the origin of the root CS

  // all points should be on top of each other

  CHECK_FALSE(
      get_transformation(*root.get(), *cs2.get()).isApprox(EigenTransform::Identity()));
  CHECK(get_transformation(*root.get(), *cs3.get()).isApprox(EigenTransform::Identity()));
  CHECK(get_transformation(*root.get(), *cs4.get()).isApprox(EigenTransform::Identity()));
  CHECK(get_transformation(*cs5.get(), *cs6.get()).isApprox(EigenTransform::Identity()));

  CHECK((p1 - p2).getNorm().magnitude() == Approx(0).margin(absMargin));
  CHECK((p1 - p3).getNorm().magnitude() == Approx(0).margin(absMargin));
  CHECK((p1 - p4).getNorm().magnitude() == Approx(0).margin(absMargin));
  CHECK((p1 - p5).getNorm().magnitude() == Approx(0).margin(absMargin));
  CHECK((p1 - p6).getNorm().magnitude() == Approx(0).margin(absMargin));
}

TEST_CASE("Geometry Sphere") {
  CoordinateSystemPtr const& rootCS = get_root_CoordinateSystem();
  Point center(rootCS, {0_m, 3_m, 4_m});
  Sphere sphere(center, 5_m);

  SECTION("getCenter") {
    CHECK((sphere.getCenter().getCoordinates(rootCS) -
           QuantityVector<length_d>(0_m, 3_m, 4_m))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));
    CHECK(sphere.getRadius() / 5_m == Approx(1));
  }

  SECTION("isInside") {
    CHECK_FALSE(sphere.contains(Point(rootCS, {100_m, 0_m, 0_m})));
    CHECK(sphere.contains(Point(rootCS, {2_m, 3_m, 4_m})));
  }
}

TEST_CASE("Geometry Box") {
  CoordinateSystemPtr const& rootCS = get_root_CoordinateSystem();
  auto initialCS = make_translation(rootCS, {0_m, 0_m, 5_m});
  Point center = Point(initialCS, {0_m, 0_m, 0_m});
  Box box(initialCS, 4_m, 5_m, 6_m);

  SECTION("getCenter") {
    CHECK((box.getCenter().getCoordinates(rootCS) - center.getCoordinates(rootCS))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));
    CHECK(box.getX() / 4_m == Approx(1));
    CHECK(box.getY() / 5_m == Approx(1));
    CHECK(box.getZ() / 6_m == Approx(1));
  }

  SECTION("isInside") {
    CHECK_FALSE(box.contains(Point(rootCS, {4.5_m, 0_m, 0_m})));
    CHECK(box.contains(Point(rootCS, {0_m, 4.5_m, 0_m})));
  }

  SECTION("internal coordinate") {
    CoordinateSystemPtr const internalCS = box.getCoordinateSystem();
    auto coordinate = box.getCenter().getCoordinates(internalCS);
    CHECK(coordinate.getX() / 1_m == Approx(0));
    CHECK(coordinate.getY() / 1_m == Approx(0));
    CHECK(coordinate.getZ() / 1_m == Approx(0));
  }

  SECTION("rotation") {
    QuantityVector<length_d> const axis_z{0_m, 0_m, 1_m};
    box.rotate(axis_z, 90);
    CHECK(box.contains(Point(rootCS, {4.5_m, 0_m, 0_m})));
    CHECK_FALSE(box.contains(Point(rootCS, {0_m, 4.5_m, 0_m})));
  }

  SECTION("from different coordinate") {
    QuantityVector<length_d> const axis_z{0_m, 0_m, 1_m};
    auto rotatedCS = make_rotation(initialCS, axis_z, 90);
    Point center(rootCS, {0_m, 0_m, 5_m});
    Box box(rotatedCS, 4_m, 5_m, 6_m);
    CHECK(box.contains(Point(rootCS, {4.5_m, 0_m, 0_m})));
    CHECK_FALSE(box.contains(Point(rootCS, {0_m, 4.5_m, 0_m})));
  }
}

TEST_CASE("Geometry SeparationPlane") {
  CoordinateSystemPtr const& rootCS = get_root_CoordinateSystem();
  Point const planeCenter = Point(rootCS, {0_m, 0_m, 0_m});

  SECTION("constructor") {
    DirectionVector const planeNorm{rootCS, {0, 0, 1}};
    SeparationPlane const sepPlane{Plane(planeCenter, planeNorm)};
    CHECK(sepPlane.asString() != "");
  }

  SECTION("isInside") {
    DirectionVector const planeNorm{rootCS, {0, 0, 1}};
    SeparationPlane const sepPlane{Plane(planeCenter, planeNorm)};

    CHECK_FALSE(sepPlane.contains(Point(rootCS, {0_m, 0_m, 1_m})));
    CHECK_FALSE(sepPlane.contains(Point(rootCS, {1_m, 0_m, 1_m})));
    CHECK_FALSE(sepPlane.contains(Point(rootCS, {-1_m, 0_m, 1_m})));
    CHECK_FALSE(sepPlane.contains(Point(rootCS, {0_m, 1_m, 1_m})));
    CHECK_FALSE(sepPlane.contains(Point(rootCS, {0_m, -1_m, 1_m})));

    CHECK(sepPlane.contains(Point(rootCS, {0_m, 0_m, -1_m})));
    CHECK(sepPlane.contains(Point(rootCS, {1_m, 0_m, -1_m})));
    CHECK(sepPlane.contains(Point(rootCS, {-1_m, 0_m, -1_m})));
    CHECK(sepPlane.contains(Point(rootCS, {0_m, 1_m, -1_m})));
    CHECK(sepPlane.contains(Point(rootCS, {0_m, -1_m, -1_m})));
  }

  SECTION("getPlane") {
    DirectionVector const planeNorm{rootCS, {0, 0, 1}};
    SeparationPlane const sepPlane{Plane(planeCenter, planeNorm)};

    auto planeCheck = sepPlane.getPlane();
  }
}

TEST_CASE("Geometry Trajectories") {
  CoordinateSystemPtr rootCS = get_root_CoordinateSystem();
  Point r0(rootCS, {0_m, 0_m, 0_m});
  // Create a particle and and a stack so we can test .getTime() method
  const Code particle{Code::Electron};
  test::Stack stack;
  // the mass of the particle
  const auto pmass{get_mass(particle)};
  // set an arbitrary energy value
  const HEPEnergyType E0{1_TeV};
  // compute the corresponding momentum
  const HEPMomentumType P0{sqrt(E0 * E0 - pmass * pmass)};
  // create the momentum vector
  const auto plab{MomentumVector(rootCS, {0_GeV, 0_GeV, P0})};
  // create an arbitrary location of the particle
  const Point pos(rootCS, 50_m, 10_m, 80_m);
  // add it finally to the stack
  auto const particle1{stack.addParticle(
      std::make_tuple(particle, calculate_kinetic_energy(plab.getNorm(), pmass),
                      plab.normalized(), pos, 0_ns))};

  SECTION("Line") {
    SpeedType const V0 = 3_m / second;
    VelocityVector v0(rootCS, {V0, 0_m / second, 0_m / second});

    Line const line(r0, v0);
    CHECK(
        (line.getPosition(2_s).getCoordinates() - QuantityVector<length_d>(6_m, 0_m, 0_m))
            .getNorm()
            .magnitude() == Approx(0).margin(absMargin));

    CHECK((line.getPositionFromArclength(4_m).getCoordinates() -
           QuantityVector<length_d>(4_m, 0_m, 0_m))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));

    CHECK((line.getPosition(7_s) -
           line.getPositionFromArclength(line.getArcLength(0_s, 7_s)))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));

    CHECK((line.getTimeFromArclength(10_m) / (10_m / v0.getNorm()) == Approx(1)));

    auto const t = 1_s;
    StraightTrajectory base(line, t);
    CHECK(line.getPosition(t).getCoordinates() == base.getPosition(1.).getCoordinates());
    // test the getTime() method for straight trajectory
    CHECK(base.getTime(particle1, 1) / 1_s == Approx(1));

    CHECK((base.getDirection(0).getComponents(rootCS) -
           QuantityVector<dimensionless_d>{1, 0, 0})
              .getNorm() == Approx(0).margin(absMargin));

    base.setDuration(0_s);

    CHECK(base.getDuration() / 1_s == Approx(0));
    CHECK(base.getLength() / 1_m == Approx(0));

    base.setDuration(10_s);

    CHECK(base.getDuration() / 1_s == Approx(10));

    StraightTrajectory base2(line,
                             std::numeric_limits<TimeType::value_type>::infinity() * 1_s);
    base2.setDuration(10_s);
    CHECK(base2.getDuration() / 1_s == Approx(10));
    // test the getTime() method for straight trajectory
    CHECK(base2.getTime(particle1, 0) / 1_s == Approx(0));

    base2.setLength(1.3_m);
    CHECK(base2.getDuration() * V0 / meter == Approx(1.3));
    CHECK(base2.getLength() / meter == Approx(1.3));
  }

  SECTION("Helix") {
    VelocityVector const vPar(rootCS, {0_m / second, 0_m / second, 4_m / second});

    VelocityVector const vPerp(rootCS, {3_m / second, 0_m / second, 0_m / second});

    auto const T = 1_s;
    auto const omegaC = 2 * M_PI / T;

    Helix const helix(r0, omegaC, vPar, vPerp);

    CHECK((helix.getPosition(1_s).getCoordinates() -
           QuantityVector<length_d>(0_m, 0_m, 4_m))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));

    CHECK((helix.getPosition(0.25_s).getCoordinates() -
           QuantityVector<length_d>(-3_m / (2 * M_PI), -3_m / (2 * M_PI), 1_m))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));

    CHECK((helix.getPosition(7_s) -
           helix.getPositionFromArclength(helix.getArcLength(0_s, 7_s)))
              .getNorm()
              .magnitude() == Approx(0).margin(absMargin));
  }

  SECTION("LeapFrog Trajectory") {

    // Create a velocity Vector
    VelocityVector v0(rootCS, {5e+2_m / second, 5e+2_m / second, 5e+2_m / second});
    // Create a magnetic filed Vector
    Vector B0(rootCS, 5_T, 5_T, 5_T);
    // provide a k constant and a random time for the LeapFrog Trajectory
    auto const k{1_m * ((1_m) / ((1_s * 1_s) * 1_V))};
    auto const t = 1e-12_s;

    // construct a LeapFrog Trajectory
    LeapFrogTrajectory base(pos, v0, B0, k, t);

    // test the getTime() method for trajectories
    CHECK((base.getTime(particle1, 1) - t) / 1_s == Approx(0));
    CHECK(base.getTime(particle1, 0) / 1_s == Approx(0));
    CHECK((base.getTime(particle1, 0) + t) / 1_s == Approx(1e-12));
  }
}

TEST_CASE("Distance between points") {
  // define a known CS
  CoordinateSystemPtr root = get_root_CoordinateSystem();

  // define known points
  Point p1(root, {0_m, 0_m, 0_m});
  Point p2(root, {0_m, 0_m, 5_m});
  Point p3(root, {1_m, 0_m, 0_m});
  Point p4(root, {5_m, 0_m, 0_m});
  Point p5(root, {0_m, 4_m, 0_m});
  Point p6(root, {0_m, 5_m, 0_m});

  // check distance() function
  CHECK(distance(p1, p2) / 1_m == Approx(5));
  CHECK(distance(p3, p4) / 1_m == Approx(4));
  CHECK(distance(p5, p6) / 1_m == Approx(1));
}

TEST_CASE("Path") {
  // define a known CS
  CoordinateSystemPtr root = get_root_CoordinateSystem();

  // define known points
  Point p1(root, {0_m, 0_m, 0_m});
  Point p2(root, {0_m, 0_m, 1_m});
  Point p3(root, {0_m, 0_m, 2_m});
  Point p4(root, {0_m, 0_m, 3_m});
  Point p5(root, {0_m, 0_m, 4_m});
  // define paths
  Path P1(p1);
  Path P2({p1, p2});
  Path P3({p1, p2, p3});
  // define deque that include point(s)
  std::deque<Point> l1 = {p1};
  std::deque<Point> l2 = {p1, p2};
  std::deque<Point> l3 = {p1, p2, p3};

  // test the various path constructors
  SECTION("Test Constructors") {
    // check constructor for one point
    CHECK(std::equal(P1.begin(), P1.end(), l1.begin(),
                     [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; }));
    // check constructor for collection of points
    CHECK(std::equal(P3.begin(), P3.end(), l3.begin(),
                     [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; }));
  }

  // test the length and access methods
  SECTION("Test getLength() and modifications to Path") {
    P1.addToEnd(p2);
    P2.removeFromEnd();
    // Check modifications to path
    CHECK(std::equal(P1.begin(), P1.end(), l2.begin(),
                     [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; }));
    CHECK(std::equal(P2.begin(), P2.end(), l1.begin(),
                     [](Point a, Point b) { return (a - b).getNorm() / 1_m < 1e-5; }));
    // Check GetStart(), GetEnd(), GetPoint()
    CHECK((P3.getEnd() - P3.getStart()).getNorm() / 1_m == Approx(2));
    CHECK((P1.getPoint(1) - p2).getNorm() / 1_m == Approx(0));
    // Check GetLength()
    CHECK(P1.getLength() / 1_m == Approx(1));
    CHECK(P2.getLength() / 1_m == Approx(0));
    CHECK(P3.getLength() / 1_m == Approx(2));
    P2.removeFromEnd();
    CHECK(P2.getLength() / 1_m == Approx(0)); // Check the length of an empty path
    P3.addToEnd(p4);
    P3.addToEnd(p5);
    CHECK(P3.getLength() / 1_m == Approx(4));
    P3.removeFromEnd();
    CHECK(P3.getLength() / 1_m == Approx(3)); // Check RemoveFromEnd() else case
    // Check GetNSegments()
    CHECK(P3.getNSegments() - 3 == Approx(0));
    P3.removeFromEnd();
    P3.removeFromEnd();
    P3.removeFromEnd();
    CHECK(P3.getNSegments() == Approx(0));
  }
}
